package listeners

import (
	"context"
	"fmt"
	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/gin-gonic/gin"
	"log"
	"math/big"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

//func init() {
//	geth = new(GethInfo)
//	addresses = make(map[string]Address)
//	geth.TotalEth = big.NewInt(0)
//}

type GethInfo struct {
	GethServer       string
	ContractsCreated int64
	TokenTransfers   int64
	ContractCalls    int64
	EthTransfers     int64
	BlockSize        float64
	LoadTime         float64
	TotalEth         *big.Int
	CurrentBlock     *types.Block
	Sync             *ethereum.SyncProgress
	LastBlockUpdate  time.Time
	SugGasPrice      *big.Int
	PendingTx        uint
	NetworkId        *big.Int
}

type Address struct {
	Balance *big.Int
	Address string
	Nonce   uint64
}

func RegisterMetrics(router *gin.Engine) {
	//rootGroup := getRootGroup(router)
	//metric := rootGroup.Group("/metric")
	//
	//metric.Any("/chains/:chainname/:port/metrics", doShowMetrics)
	router.Any("/metrics", doShowMetrics)
	router.POST("/metrics/chains/:chain/nodes/:node/:ip/:port", addMetricAddress)
	router.DELETE("/metrics/chains/", removeAllMetricAddress)
}

type ChainNodeMetricAddress struct {
	Chain string `json:"chain"`
	Node  string `json:"node"`
	Ip    string `json:"ip"`
	Port  string `json:"port"`
}

var (
	cnmaMapper = make(map[string]ChainNodeMetricAddress)
)

func removeAllMetricAddress(ctx *gin.Context) {
	cnmaMapper = make(map[string]ChainNodeMetricAddress)

	ctx.JSON(http.StatusOK, "")
}

func addMetricAddress(ctx *gin.Context) {
	// http://192.168.200.102:8545
	chain := ctx.Param("chain")
	node := ctx.Param("node")
	ip := ctx.Param("ip")
	port := ctx.Param("port")
	if chain == "" || node == "" || ip == "" || port == "" {
		ctx.JSON(http.StatusBadRequest, NewErrorMessage(Metrics, 1, "addMetric", fmt.Sprintf("bad parameters: chain,node,ip,port")))
		return
	}

	cnmaKey := fmt.Sprintf("%s-%s", chain, node)
	cnmaMapper[cnmaKey] = ChainNodeMetricAddress{chain, node, ip, port}

	ctx.JSON(http.StatusOK, "")
}

//func doShowFake(ctx *gin.Context) {
//	var allOut []string
//
//	allOut = append(allOut, "geth_block{chain=\"foo\" node=\"bar\"} 455")
//	allOut = append(allOut, "geth_seconds_last_block{chain=\"foo\" node=\"bar\"} 1.50")
//	allOut = append(allOut, "geth_block_transactions{chain=\"foo\" node=\"bar\"} 0")
//	allOut = append(allOut, "geth_block_value{chain=\"foo\" node=\"bar\"} 0")
//	allOut = append(allOut, "geth_block_gas_used{chain=\"foo\" node=\"bar\"} 0")
//	allOut = append(allOut, "geth_block_gas_limit{chain=\"foo\" node=\"bar\"} 4712388")
//	allOut = append(allOut, "geth_block_nonce{chain=\"foo\" node=\"bar\"} 8259203030848160882")
//	allOut = append(allOut, "geth_block_difficulty{chain=\"foo\" node=\"bar\"} 565099")
//	allOut = append(allOut, "geth_block_uncles{chain=\"foo\" node=\"bar\"} 0")
//	allOut = append(allOut, "geth_block_size_bytes{chain=\"foo\" node=\"bar\"} 537000")
//	allOut = append(allOut, "geth_gas_price{chain=\"foo\" node=\"bar\"} 18000000000")
//	allOut = append(allOut, "geth_pending_transactions{chain=\"foo\" node=\"bar\"} 0")
//	allOut = append(allOut, "geth_network_id{chain=\"foo\" node=\"bar\"} 1999")
//	allOut = append(allOut, "geth_contracts_created{chain=\"foo\" node=\"bar\"} 0")
//	allOut = append(allOut, "geth_token_transfers{chain=\"foo\" node=\"bar\"} 0")
//	allOut = append(allOut, "geth_eth_transfers{chain=\"foo\" node=\"bar\"} 0")
//	allOut = append(allOut, "geth_load_time{chain=\"foo\" node=\"bar\"} 0.0816")
//
//	w := ctx.Writer
//	w.Write([]byte(strings.Join(allOut, "\n")))
//}

func doShowMetrics(ctx *gin.Context) {
	//defer func() {
	//	if e := recover(); e != nil {
	//		log.Printf("panic: %v", e)
	//	}
	//}()

	// http://192.168.200.102:8545
	//chainname := ctx.Query("chainname")
	//node := ctx.Param("node")
	//ip := ctx.Param("ip")
	//port := ctx.Param("port")
	//if chainname == "" {
	//	ctx.JSON(http.StatusBadRequest, NewErrorMessage(Metrics, 1, "doShowMetrics", fmt.Sprintf("bad parameter: chainname,port")))
	//	return
	//}

	for k, chainNodeMetricAddress := range cnmaMapper {
		log.Printf("calculate metric for %s", k)

		var (
			eth               *ethclient.Client
			geth              *GethInfo
			delay             int
			watchingAddresses string
			addresses         map[string]Address
		)

		geth = new(GethInfo)
		addresses = make(map[string]Address)
		geth.TotalEth = big.NewInt(0)

		var err error
		//geth.GethServer = fmt.Sprintf("http://%s:%s", "192.168.200.30", port)
		geth.GethServer = fmt.Sprintf("http://%s:%s", chainNodeMetricAddress.Ip, chainNodeMetricAddress.Port)
		watchingAddresses = os.Getenv("ADDRESSES")
		delay, _ = strconv.Atoi(os.Getenv("DELAY"))
		if delay == 0 {
			delay = 1500
		}
		log.Printf("Connecting to Ethereum node: %v\n", geth.GethServer)
		eth, err = ethclient.Dial(geth.GethServer)
		if err != nil {
			log.Println(NewErrorMessage(Metrics, 2, "doShowMetrics", fmt.Sprintf("nBlockByNumber: %v", err)).Json())
			ctx.JSON(http.StatusInternalServerError, NewErrorMessage(Metrics, 2, "doShowMetrics", fmt.Sprintf("not dial: %v", err)).Json())
			return
		}
		defer eth.Close()

		geth.CurrentBlock, err = eth.BlockByNumber(context.TODO(), nil)
		if err != nil {
			//ctx.JSON(http.StatusInternalServerError, NewErrorMessage(Metrics, 3, "doShowMetrics", fmt.Sprintf("nBlockByNumber: %v", err)))
			//return
			log.Println(NewErrorMessage(Metrics, 3, "doShowMetrics", fmt.Sprintf("nBlockByNumber: %v", err)).Json())
			ctx.JSON(http.StatusInternalServerError, "")
			return
		}

		Routine(eth, geth, delay, watchingAddresses, addresses)
		//MetricsHttp(geth, addresses, ctx.Writer, ctx.Request, chainNodeMetricAddress)
		MetricsHttp(geth, addresses, ctx, chainNodeMetricAddress)
	}

}

// HTTP response handler for /metrics
func MetricsHttp(geth *GethInfo, addresses map[string]Address, ctx *gin.Context, chainNodeMetricAddress ChainNodeMetricAddress) {
	var allOut []string
	block := geth.CurrentBlock
	if block == nil {
		//w.WriteHeader(http.StatusInternalServerError)
		//w.Write([]byte(fmt.Sprintf("issue receiving block from URL: %v", geth.GethServer)))
		ctx.JSON(400, NewErrorMessage(Metrics, 1, "MetricsHttp", fmt.Sprintf("issue receiving block from URL: %v", geth.GethServer)).Json())
		return
	}
	CalculateTotals(geth, block)

	categoryExp := fmt.Sprintf("{chain=\"%s\",node=\"%s\",ip=\"%s\",port=\"%s\"}", chainNodeMetricAddress.Chain, chainNodeMetricAddress.Node, chainNodeMetricAddress.Ip, chainNodeMetricAddress.Port)

	allOut = append(allOut, fmt.Sprintf("geth_block%s %v", categoryExp, block.NumberU64()))
	allOut = append(allOut, fmt.Sprintf("geth_seconds_last_block%s %0.2f", categoryExp, time.Now().Sub(geth.LastBlockUpdate).Seconds()))
	allOut = append(allOut, fmt.Sprintf("geth_block_transactions%s %v", categoryExp, len(block.Transactions())))
	allOut = append(allOut, fmt.Sprintf("geth_block_value%s %v", categoryExp, ToEther(geth.TotalEth)))
	allOut = append(allOut, fmt.Sprintf("geth_block_gas_used%s %v", categoryExp, block.GasUsed()))
	allOut = append(allOut, fmt.Sprintf("geth_block_gas_limit%s %v", categoryExp, block.GasLimit()))
	allOut = append(allOut, fmt.Sprintf("geth_block_nonce%s %v", categoryExp, block.Nonce()))
	allOut = append(allOut, fmt.Sprintf("geth_block_difficulty%s %v", categoryExp, block.Difficulty()))
	allOut = append(allOut, fmt.Sprintf("geth_block_uncles%s %v", categoryExp, len(block.Uncles())))
	allOut = append(allOut, fmt.Sprintf("geth_block_size_bytes%s %v", categoryExp, geth.BlockSize))
	allOut = append(allOut, fmt.Sprintf("geth_gas_price%s %v", categoryExp, geth.SugGasPrice))
	allOut = append(allOut, fmt.Sprintf("geth_pending_transactions%s %v", categoryExp, geth.PendingTx))
	allOut = append(allOut, fmt.Sprintf("geth_network_id%s %v", categoryExp, geth.NetworkId))
	allOut = append(allOut, fmt.Sprintf("geth_contracts_created%s %v", categoryExp, geth.ContractsCreated))
	allOut = append(allOut, fmt.Sprintf("geth_token_transfers%s %v", categoryExp, geth.TokenTransfers))
	allOut = append(allOut, fmt.Sprintf("geth_eth_transfers%s %v", categoryExp, geth.EthTransfers))
	allOut = append(allOut, fmt.Sprintf("geth_load_time%s %0.4f", categoryExp, geth.LoadTime))

	if geth.Sync != nil {
		allOut = append(allOut, fmt.Sprintf("geth_known_states%s %v", categoryExp, int(geth.Sync.KnownStates)))
		allOut = append(allOut, fmt.Sprintf("geth_highest_block%s %v", categoryExp, int(geth.Sync.HighestBlock)))
		allOut = append(allOut, fmt.Sprintf("geth_pulled_states%s %v", categoryExp, int(geth.Sync.PulledStates)))
	}

	//for _, v := range addresses {
	//	allOut = append(allOut, fmt.Sprintf("geth_address_balance{address=\"%v\"} %v", v.Address, ToEther(v.Balance).String()))
	//	allOut = append(allOut, fmt.Sprintf("geth_address_nonce{address=\"%v\"} %v", v.Address, v.Nonce))
	//}

	//w.Write([]byte(strings.Join(allOut, "\n")))
	ctx.Data(http.StatusOK, "", append([]byte(strings.Join(allOut, "\n")), byte('\n')))
}

func Routine(eth *ethclient.Client, geth *GethInfo, delay int, watchingAddresses string, addresses map[string]Address) {
	var lastBlock *types.Block
	ctx := context.Background()
	//for {
	t1 := time.Now()
	var err error
	geth.CurrentBlock, err = eth.BlockByNumber(ctx, nil)
	if err != nil {
		log.Printf("issue with reponse from geth server: %v\n", geth.CurrentBlock)
		time.Sleep(time.Duration(delay) * time.Millisecond)
		//continue
	}
	geth.SugGasPrice, _ = eth.SuggestGasPrice(ctx)
	//log.Printf("SugGasPrice: %v", geth.SugGasPrice)
	geth.PendingTx, _ = eth.PendingTransactionCount(ctx)
	//log.Printf("PendingTx: %v", geth.PendingTx)
	geth.NetworkId, _ = eth.NetworkID(ctx)
	//log.Printf("NetworkId: %v", geth.NetworkId)
	geth.Sync, _ = eth.SyncProgress(ctx)
	//log.Printf("Sync: %v", geth.Sync)

	if lastBlock == nil || geth.CurrentBlock.NumberU64() > lastBlock.NumberU64() {
		log.Printf("Received block #%v with %v transactions (%v)\n", geth.CurrentBlock.NumberU64(), len(geth.CurrentBlock.Transactions()), geth.CurrentBlock.Hash().String())
		geth.LastBlockUpdate = time.Now()
		//log.Printf("LastBlockUpdate: %v", geth.LastBlockUpdate)
		geth.LoadTime = time.Now().Sub(t1).Seconds()
		//log.Printf("LoadTime: %v", geth.LoadTime)
	}

	if watchingAddresses != "" {
		for _, a := range strings.Split(watchingAddresses, ",") {
			addr := common.HexToAddress(a)
			balance, _ := eth.BalanceAt(ctx, addr, geth.CurrentBlock.Number())
			nonce, _ := eth.NonceAt(ctx, addr, geth.CurrentBlock.Number())
			address := Address{
				Address: addr.String(),
				Balance: balance,
				Nonce:   nonce,
			}
			addresses[a] = address
		}
	}

	lastBlock = geth.CurrentBlock
	//log.Printf("lastBlock: %v", lastBlock)
	time.Sleep(time.Duration(delay) * time.Millisecond)
	//log.Println("444444444444444444")
	//}
}

// stringToFloat will simply convert a string to a float
func stringToFloat(s string) float64 {
	amount, _ := strconv.ParseFloat(s, 10)
	return amount
}

// CONVERTS WEI TO ETH
func ToEther(o *big.Int) *big.Float {
	pul, int := big.NewFloat(0), big.NewFloat(0)
	int.SetInt(o)
	pul.Mul(big.NewFloat(0.000000000000000001), int)
	return pul
}

func CalculateTotals(geth *GethInfo, block *types.Block) {
	geth.TotalEth = big.NewInt(0)
	geth.ContractsCreated = 0
	geth.TokenTransfers = 0
	geth.EthTransfers = 0
	for _, b := range block.Transactions() {

		if b.To() == nil {
			geth.ContractsCreated++
		}

		if len(b.Data()) >= 4 {
			method := hexutil.Encode(b.Data()[:4])
			if method == "0xa9059cbb" {
				geth.TokenTransfers++
			}
		}

		if b.Value().Sign() == 1 {
			geth.EthTransfers++
		}

		geth.TotalEth.Add(geth.TotalEth, b.Value())
	}

	//size := strings.Split(geth.CurrentBlock.Size().String(), " ")
	//geth.BlockSize = stringToFloat(size[0]) * 1000
	geth.BlockSize = float64(geth.CurrentBlock.Size() * 1000)
}
